package org.apache.lucene.search;

/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.io.IOException;
import java.util.ArrayList;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;

class ConstantScoreAutoRewrite extends TermCollectingRewrite<BooleanQuery> {

    // Defaults derived from rough tests with a 20.0 million
    // doc Wikipedia index.  With more than 350 terms in the
    // query, the filter method is fastest:
    public static int DEFAULT_TERM_COUNT_CUTOFF = 350;

    // If the query will hit more than 1 in 1000 of the docs
    // in the index (0.1%), the filter method is fastest:
    public static double DEFAULT_DOC_COUNT_PERCENT = 0.1;

    private int termCountCutoff = DEFAULT_TERM_COUNT_CUTOFF;
    private double docCountPercent = DEFAULT_DOC_COUNT_PERCENT;

    /** If the number of terms in this query is equal to or
     *  larger than this setting then {@link
     *  #CONSTANT_SCORE_FILTER_REWRITE} is used. */
    public void setTermCountCutoff(int count) {
        termCountCutoff = count;
    }

    /** @see #setTermCountCutoff */
    public int getTermCountCutoff() {
        return termCountCutoff;
    }

    /** If the number of documents to be visited in the
     *  postings exceeds this specified percentage of the
     *  maxDoc() for the index, then {@link
     *  #CONSTANT_SCORE_FILTER_REWRITE} is used.
     *  @param percent 0.0 to 100.0 */
    public void setDocCountPercent(double percent) {
        docCountPercent = percent;
    }

    /** @see #setDocCountPercent */
    public double getDocCountPercent() {
        return docCountPercent;
    }

    @Override
    protected BooleanQuery getTopLevelQuery() {
        return new BooleanQuery(true);
    }

    @Override
    protected void addClause(BooleanQuery topLevel, Term term, float boost /*ignored*/) {
        topLevel.add(new TermQuery(term), BooleanClause.Occur.SHOULD);
    }

    @Override
    public Query rewrite(final IndexReader reader, final MultiTermQuery query) throws IOException {

        // Get the enum and start visiting terms.  If we
        // exhaust the enum before hitting either of the
        // cutoffs, we use ConstantBooleanQueryRewrite; else,
        // ConstantFilterRewrite:
        final int docCountCutoff = (int) ((docCountPercent / 100.) * reader.maxDoc());
        final int termCountLimit = Math.min(BooleanQuery.getMaxClauseCount(), termCountCutoff);

        final CutOffTermCollector col = new CutOffTermCollector(reader, docCountCutoff, termCountLimit);
        collectTerms(reader, query, col);

        if (col.hasCutOff) {
            return MultiTermQuery.CONSTANT_SCORE_FILTER_REWRITE.rewrite(reader, query);
        } else {
            final Query result;
            if (col.pendingTerms.isEmpty()) {
                result = getTopLevelQuery();
            } else {
                BooleanQuery bq = getTopLevelQuery();
                for (Term term : col.pendingTerms) {
                    addClause(bq, term, 1.0f);
                }
                // Strip scores
                result = new ConstantScoreQuery(bq);
                result.setBoost(query.getBoost());
            }
            query.incTotalNumberOfTerms(col.pendingTerms.size());
            return result;
        }
    }

    private static final class CutOffTermCollector implements TermCollector {
        CutOffTermCollector(IndexReader reader, int docCountCutoff, int termCountLimit) {
            this.reader = reader;
            this.docCountCutoff = docCountCutoff;
            this.termCountLimit = termCountLimit;
        }

        public boolean collect(Term t, float boost) throws IOException {
            pendingTerms.add(t);
            // Loading the TermInfo from the terms dict here
            // should not be costly, because 1) the
            // query/filter will load the TermInfo when it
            // runs, and 2) the terms dict has a cache:
            docVisitCount += reader.docFreq(t);
            if (pendingTerms.size() >= termCountLimit || docVisitCount >= docCountCutoff) {
                hasCutOff = true;
                return false;
            }
            return true;
        }

        int docVisitCount = 0;
        boolean hasCutOff = false;

        final IndexReader reader;
        final int docCountCutoff, termCountLimit;
        final ArrayList<Term> pendingTerms = new ArrayList<Term>();
    }

    @Override
    public int hashCode() {
        final int prime = 1279;
        return (int) (prime * termCountCutoff + Double.doubleToLongBits(docCountPercent));
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;

        ConstantScoreAutoRewrite other = (ConstantScoreAutoRewrite) obj;
        if (other.termCountCutoff != termCountCutoff) {
            return false;
        }

        if (Double.doubleToLongBits(other.docCountPercent) != Double.doubleToLongBits(docCountPercent)) {
            return false;
        }

        return true;
    }
}
